WebRTC is quite a fascinating tech, and I was able to get hands on it a few weeks ago. It was an existing project, and some parts of WebRTC were already implemented. But one thing had a problem: users are able to connect to each other, but audio/video communication was flaky and working purely on luck. Sometimes it works between some users, and some do not.
After trying for quite a few back-and-forths between LLM and hitting my head on my rubber duck, I could find only one answer. I have to do the forbidden task that has been banned after Vibe Coding. It's what everyone talks about on an app whose name is the third last character of the alphabet. I have read the documentation of WebRTC.
Although I could not grasp all of it at first, I watched a couple of videos on it and then finally narrowed down the cause to two things
- The handshake (SDP) is failing due to a lack of STUN servers.
- There's literally no TURN Server in the setup.
Who is the STUN server?
This server helps to find the public IP address of your device. You might be thinking, "How does your own device not know the public IP address already?" Well, NO. Your device, if connected to WIFI or LAN networks, is behind a NAT. I am not getting deep into networking stuff. NAT just hides your public address from the outside world, and your device does not know which public address and port your NAT has assigned for public. WebRTC needs the public address of your NAT to connect with other peers to form a P2P (Peer to Peer) connection. Again, not getting deep into the P2P connection; just think of it like P2P is one of the sauces to send you video/audio packets in blazing fast under 200ms.
So, your browser sends a request to the STUN server, and the STUN server returns the IP and PORT number, and WebRTC can use it. Previous implementation using Google's STUN servers, which is nothing wrong in our scale, but I have added some more fallback servers.
This added some success in some devices but did not mitigate it. So, TURN Server is the last option I have.
1// A basic array of free fallback STUN servers2const fallbackStunServers: RTCIceServer[] = [3 { urls: 'stun:stun.l.google.com:19302' },4 { urls: 'stun:stun1.l.google.com:19302' },5 { urls: 'stun:stun2.l.google.com:19302' },6];
TURN Server ๐
In some restricted environments, direct P2P connections cannot be established due to corporate firewalls and mobile networks (which hate UDP like it's her ex), and in my case, hehehe! You know how corporate wifi works.
After some findings, I have settled with Metered.ca for the TURN servers. In this setup, this TURN server just returns ICE candidates, which WebRTC can use as last resorts. My backend fetches ICE servers from the metered.ca endpoint, caches them, and then returns to the frontend.
1use Illuminate\Support\Facades\Http; 2use Illuminate\Support\Facades\Cache; 3class WebRTCController extends Controller 4{ 5 public function getIceServers() 6 { 7 // Cache the credentials for an hour so we aren't spamming the API 8 $iceServers = Cache::remember('metered_ice_servers', 3600, function () { 9 $apiKey = config('services.metered.key');10 $url = "https://[your-app-name].metered.live/api/v1/turn/credentials?apiKey={$apiKey}";11 12 $response = Http::get($url);13 14 return $response->json();15 });16 return response()->json($iceServers);17 }18}
I merged this ICE candidate with my other existing ICE candidates, and it just works for now.
1async function initializeWebRTC(): Promise<RTCPeerConnection> { 2 // 1. Fetch the cached TURN servers from our Laravel backend 3 const response = await fetch('/api/webrtc/ice-servers'); 4 const turnServers: RTCIceServer[] = await response.json(); 5 // 2. Merge our free STUN servers with the premium TURN servers 6 const iceServers: RTCIceServer[] = [ 7 ...fallbackStunServers, 8 ...turnServers 9 ];10 // 3. Pass them to the WebRTC Configuration11 const configuration: RTCConfiguration = {12 iceServers13 };14 // 4. Initialize the connection15 const peerConnection = new RTCPeerConnection(configuration);16 17 return peerConnection;18}
Some thoughts
And some of you might also be asking why I am creating a P2P connection.
In our use case the maximum number of people that will join is 3; if I include candidate screenshares, that's a total of 4 x (4 - 1) = 12 streams and 6 connections, which can be handled by modern hardware and wifi bandwidth.
Statistically around 10-12% of global WebRTC traffic cannot be connected via P2P, so if all of our devices fall under that percentage, we have to find a better solution. Until then, thanks for reading this.
Syntax highlighting by Torchlight.dev
Kushal Saha
I'm Kushal, a full-stack software engineer based in Kolkata specializing in the Laravel ecosystem and modern frontends. I advocate for clean and type-safe code, preferring to build scalable applications. I use this space to document my technical projects, share development insights, and explore the tools that power the web.